import re
from typing import List, Optional, Tuple

import asyncssh

from app.bot.services.helpers import (
    generate_iponly_proxy_string,
    generate_proxy_string,
    generate_reload_file_content,
)
from app.bot.services.loggers import logger
from app.database.enums import ServerPrivacy, ServerType
from app.database.models import AllowedIP, Proxy, Server, VPNServer


class RemoteClient:
    """Client to interact with a remote host via SSH."""

    def __init__(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
    ):
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.client = None

    @property
    async def connection(self):
        """Open SSH connection to remote host."""
        try:
            client = await asyncssh.connect(
                host=self.host,
                port=self.port,
                username=self.username,
                password=self.password,
                known_hosts=None,
            )
            logger.info(f"Подключился к серверу {self.host}:{self.port}")
            return client

        except asyncssh.DisconnectError as e:
            logger.error(
                f"DisconnectError occurred; Error code: {e.code}; Reason: {e.reason}"
            )
        except Exception as e:
            logger.error(
                f"Unexpected error while connecting to host {self.host}; Error message: {e}"
            )

    @staticmethod
    async def read_file(
        conn: asyncssh.SSHClientConnection, filepath: str
    ) -> str | list[str] | bool:
        """
        Read file from remote host.
        :param asyncssh.SSHClientConnection conn: Connection to server
        :param str filepath: Path to file hosted on remote server to fetch.
        """

        async with conn.start_sftp_client() as sftp:
            try:
                remote_file = await sftp.open(filepath)
                return await remote_file.read()

            except (OSError, asyncssh.Error, asyncssh.sftp.SFTPFailure) as e:
                logger.exception("SFTP reading file failed: " + str(e))

            except Exception as e:
                logger.exception(f"Unexpected error reading file: {str(e)}")

        return False

    @staticmethod
    async def write_file(
        conn: asyncssh.SSHClientConnection, filepath: str, content: str
    ) -> bool:
        """
        Write remote file.
        :param str filepath: Path to file hosted on remote server to fetch.
        :param str content: Content of new file
        """

        async with conn.start_sftp_client() as sftp:
            try:
                remote_file = await sftp.open(filepath, "w")
                await remote_file.write(content)
                return True

            except (OSError, asyncssh.Error, asyncssh.sftp.SFTPFailure) as e:
                logger.exception(f"SFTP writing file failed: {str(e)}")

            except Exception as e:
                logger.exception(f"Unexpected error writing file: {str(e)}")

        return False

    @staticmethod
    async def execute_command(
        conn: asyncssh.SSHClientConnection, command: str
    ) -> str | bool:
        """
        Execute command on server.
        :param List[str] command: Unix command.
        """

        try:
            result = await conn.run(command, check=True)
            if result.exit_status == 0:
                return result.stdout

        except (OSError, asyncssh.Error) as e:
            logger.exception(f"Error executing command: {str(e)}")

        except Exception as e:
            logger.exception(f"Unexpected error executing command: {str(e)}")

        return False

    @staticmethod
    async def write_cron(
        conn: asyncssh.SSHClientConnection,
        cron_file_path: Optional[str] = "/usr/local/cron.txt",
    ) -> bool:
        """
        Replacing crontab from txt file
        :param asyncssh.SSHClientConnection conn: Connection to server
        :param Optional[str] cron_file_path: Path to txt file containing cron jobs
        """
        try:
            cmd = f"crontab < {cron_file_path}"
            result = await conn.run(cmd)
            if result.exit_status == 0:
                return True

        except (OSError, asyncssh.Error) as e:
            logger.exception(f"Error writing cron: {str(e)}")

        except Exception as e:
            logger.exception(f"Unexpected error writing cron: {str(e)}")

        return False

    @staticmethod
    async def get_modem_ip(
        config_content_lines: list[str], proxy_port: int
    ) -> str | bool:
        """
        Getting internal modem ip for private mobile proxy from 3proxy config file by proxy port
        :param list[str] config_content_lines: 3proxy.cfg content as list of strings
        :param int proxy_port: Port of proxy related to modem
        """
        try:
            pattern = f"-p{proxy_port} "
            for line in config_content_lines:
                if pattern in line:
                    server_line = line
                    modem_ip = re.findall("-e([\S]+)", server_line)[0].strip()
                    return modem_ip

        except Exception as e:
            logger.exception(e)

        return False

    @staticmethod
    async def get_cron_line(cron_file_content: List[str], modem_ip: str) -> str | bool:
        """
        Returns cron job containing modem ip
        :param cron_file_content: Cron file lines
        :param modem_ip: Modem to find
        """
        for cron_line in cron_file_content:
            if modem_ip.replace("00", "") in cron_line:
                return cron_line
        return False

    @staticmethod
    async def get_proxy_block(
        config_content: str,
        proxy_login: Optional[str] = None,
        proxy_password: Optional[str] = None,
        proxy_port: Optional[int] = None,
        find_by_port: bool = False,
    ) -> str | bool:
        """
        Finding proxy block from auth to flush in 3proxy config
        :param str config_content: 3proxy.cfg content
        :param Optional[str] proxy_login: Login of proxy to find
        :param Optional[str] proxy_password: Password or proxy to find
        :param Optional[int] proxy_port: Proxy port on which the block is searched
        :param bool find_by_port: Find proxy block by its port
        """
        try:
            proxy_blocks = re.findall("#*auth\ \w[\w\W]*?flush", config_content)
            if len(proxy_blocks) == 0:
                raise ValueError("В конфиге нет блоков прокси")

            if find_by_port and proxy_port:
                for proxy_block in proxy_blocks:
                    if f"-p{proxy_port} " in proxy_block:
                        return proxy_block
            else:
                for proxy_block in proxy_blocks:
                    if (
                        f"{proxy_login}:" in proxy_block
                        and f"{proxy_password}" in proxy_block
                    ):
                        return proxy_block

        except Exception as e:
            logger.exception(e)

        return False


async def change_authorization_type(
    server: Server,
    proxies: list[Proxy],
    authorization_type: str,
    old_ip: Optional[AllowedIP] = None,
    new_ip: Optional[AllowedIP] = None,
) -> bool:
    """
    Modifying 3proxy config on selected server
    Adding new line to config if authorization_type == "by_password"
    Adding allowed ip to config if authorization_type == "by_ip_address"
    :param Server server: Instance of Server to connect
    :param list[Proxy] proxies: List of proxies
    :param str authorization_type: Enum value ("by_ip_address", "by_password")
    :param Optional[AllowedIP] old_ip: Old allowed ip address
    :param Optional[AllowedIP] new_ip: New allowed ip for proxies
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )
    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(
            conn,
            server.config_path,
        )

        config_content_lines = config_content.split("\n")

        for proxy in proxies:

            if (
                server.proxy_type == ServerType.mobile
                and server.proxy_privacy == ServerPrivacy.shared
            ):
                proxy_block = await client.get_proxy_block(
                    config_content, proxy_port=proxy.port_https, find_by_port=True
                )
            else:
                proxy_block = await client.get_proxy_block(
                    config_content, proxy.login, proxy.password
                )

                if not proxy_block:
                    proxy_block = await client.get_proxy_block(
                        config_content, proxy_port=proxy.port_https, find_by_port=True
                    )
                    if not proxy_block:
                        logger.error(
                            f"Не могу найти блок прокси по порту {proxy.port_https}"
                        )
                        return False

            modem_ip = await client.get_modem_ip(config_content_lines, proxy.port_https)

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {proxy.port_https}")
                return False

            if (
                server.proxy_type == ServerType.mobile
                and server.proxy_privacy == ServerPrivacy.shared
            ):
                logger.info(f"Меняю авторизацию на шаред прокси {proxy.port_https}")
                # Заменяем авторизацию на мобильных шаред прокси
                if "iponly" in proxy_block:
                    allowed_ips = re.findall("auth iponly([\w\W]*?)proxy", proxy_block)[
                        0
                    ].strip()
                    if new_ip.ip_address not in allowed_ips:
                        if old_ip:
                            new_allowed_ips = allowed_ips.replace(
                                old_ip.ip_address, new_ip.ip_address
                            )
                            logger.info(
                                f"Заменил старый ip {old_ip.ip_address} на новый {new_ip.ip_address}"
                            )
                        else:
                            # Проверяем, есть ли в блоке прокси разрешенные ip
                            # Может быть ситуация, когда блок будет выглядеть так: auth iponly allow * ... flush
                            if len(re.findall("((?:\d+\.){3}\d+)", allowed_ips)) > 0:
                                new_allowed_ips = f"{allowed_ips},{new_ip.ip_address}"
                            else:
                                new_allowed_ips = allowed_ips.replace(
                                    "*", f"* {new_ip.ip_address}"
                                )
                                logger.info("В блоке нет других ip, добавил новый")
                            logger.info(f"Добавил в блок новый ip {new_ip.ip_address}")

                        new_proxy_block = proxy_block.replace(
                            allowed_ips, new_allowed_ips
                        )
                        config_content = config_content.replace(
                            proxy_block, new_proxy_block
                        )

                elif "strong" in proxy_block:
                    auth_proxy_block = await generate_iponly_proxy_string(
                        new_ip.ip_address, proxy.port_https, modem_ip
                    )
                    config_content = config_content.replace(
                        proxy_block, auth_proxy_block
                    )
                    logger.info(
                        f"Заменил auth strong на auth iponly с ip {new_ip.ip_address}"
                    )
            else:
                # Заменяем авторизацию на всех остальных прокси
                logger.info(f"Меняю авторизацию на приватном прокси {proxy.port_https}")
                if authorization_type == "by_ip_address":

                    if "iponly" in proxy_block:
                        current_ip = re.findall("((?:\d+\.){3}\d+)", proxy_block)[0]
                        # TODO Тут может быть больше 1 ip при переходе с шаредов
                        new_proxy_block = proxy_block.replace(
                            current_ip, new_ip.ip_address
                        )
                        config_content = config_content.replace(
                            proxy_block, new_proxy_block
                        )
                        logger.info(
                            f"Заменил старый ip {current_ip} на новый {new_ip.ip_address}"
                        )
                        # Заменяем старый ip в блоке на новый

                    if "strong" in proxy_block:
                        auth_proxy_block = await generate_iponly_proxy_string(
                            new_ip.ip_address, proxy.port_https, modem_ip
                        )
                        config_content = config_content.replace(
                            proxy_block, auth_proxy_block
                        )
                        logger.info(
                            f"Заменил auth strong на auth iponly c ip {new_ip.ip_address}"
                        )
                        # Заменяем auth strong на auth iponly

                elif authorization_type == "by_password":

                    if "iponly" in proxy_block:

                        auth_strong_proxy_block = await generate_proxy_string(
                            proxy.login,
                            proxy.password,
                            proxy.port_https,
                            modem_ip,
                            server.inner_ip_address,
                        )
                        config_content = config_content.replace(
                            proxy_block, auth_strong_proxy_block
                        )
                        logger.info(
                            f"Заменил auth iponly на auth strong с логином {proxy.login}"
                        )
                        # Заменяем auth iponly на auth srong

        config_writed = await client.write_file(
            conn, server.config_path, config_content
        )
        logger.info("Внес все изменения в конфиг")

    return config_writed


async def change_rotation_type(
    server: Server,
    proxies: list[Proxy],
    rotation_period: int,
    reload_type: str,
) -> bool:
    """
    Modifying cron file on selected server
    Commenting cron job if reload_type == "by_link"
    Changing cron job period if reload_type == "by_time" and rotation period passed
    :param Server server: Instance of Server to connect
    :param list[Proxy] proxies: List of proxies
    :param int rotation_period: Selected rotation period
    :param str reload_type: Enum value ("by_link", "by_time")
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )
    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(
            conn,
            server.config_path,
        )
        config_content_lines = config_content.split("\n")

        cron_file = await client.execute_command(conn, "crontab -l")
        cron_file_lines = cron_file.split("\n")

        for proxy in proxies:
            logger.info(f"Меняю ротацию для прокси {proxy.port_https}")

            modem_ip = await client.get_modem_ip(config_content_lines, proxy.port_https)
            modem_ip = modem_ip.replace("00", "")

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {proxy.port_https}")
                return False

            cron_line = await client.get_cron_line(cron_file_lines, modem_ip)

            if not cron_line:
                logger.error(f"Не нашел строку крона по модему {modem_ip}")
                return False

            if reload_type == "by_link":
                if "#" not in cron_line:
                    # Комментируем строку крона если авторизация по ссылке
                    new_cron_line = "#" + cron_line
                    cron_file = cron_file.replace(cron_line, new_cron_line)
                    logger.info(f"Закомментировал cron job для модема {modem_ip}")

            elif reload_type == "by_time":
                # Заменяем период срабатывания задачи крона
                new_cron_line = re.sub(
                    r"(\d+)", str(rotation_period), cron_line.replace("#", ""), count=1
                )
                cron_file = cron_file.replace(cron_line, new_cron_line)
                logger.info(
                    f"Заменил период cron job для модема {modem_ip} на {str(rotation_period)} минут"
                )

            if proxy.reload_link:
                old_path = f"/var/www/html/{proxy.reload_link}.php"
                new_path = f"/var/www/html/{proxy.reload_link}.php"

                # Проверяем, существует ли файл реконнекта
                command = f"mv {old_path} {new_path}"
                if not await client.execute_command(conn, command):
                    reload_file_content = await generate_reload_file_content(
                        server, modem_ip
                    )
                    await client.write_file(
                        conn,
                        f"/var/www/html/{proxy.reload_link}.php",
                        reload_file_content,
                    )
                    logger.info(
                        f"Создан новый файл реконнекта: {proxy.reload_link}.php"
                    )

        await client.write_file(conn, "/usr/local/cron.txt", cron_file)
        cron_writed = await client.write_cron(conn)
        logger.info("Внес все изменения в крон")

    return cron_writed


async def change_expired_proxies(
    server: Server,
    proxies: Tuple[Proxy, Proxy] | List[Proxy],
    allowed_ip: Optional[AllowedIP],
) -> bool:
    """
    Replacing old proxy credentials and reload link with new one on selected server
    Restoring cron job and authorization type to default
    :param Server server: Instance of Server to connect
    :param Tuple[Proxy] proxies: Tuple of old proxy and new Proxy instances
    :param Optional[AllowedIP] allowed_ip: Allowed ip address to delete
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(conn, server.config_path)
        config_content_lines = config_content.split("\n")

        if server.proxy_type == ServerType.mobile:
            cron_file_content = await client.execute_command(conn, "crontab -l")
            cron_file_lines = cron_file_content.split("\n")

        for new_proxy, old_proxy in proxies:
            modem_ip = await client.get_modem_ip(
                config_content_lines, old_proxy.get("port_https")
            )

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {new_proxy.port_https}")
                return False

            if (
                server.proxy_type == ServerType.mobile
                and server.proxy_privacy == ServerPrivacy.private
            ):
                cron_line = await client.get_cron_line(cron_file_lines, modem_ip)
                if not cron_line:
                    logger.error(f"Не нашел строку крона по модему {modem_ip}")
                    return False

                # Раскомментируем задачу в кроне и возвращаем дефолтный интервал ("5 минут")
                new_cron_line = re.sub(
                    r"(\d+)", "5", cron_line.replace("#", ""), count=1
                )
                cron_file_content = cron_file_content.replace(cron_line, new_cron_line)
                logger.info(f"Вернул дефолную строку крона для модема {modem_ip}")

            if (
                server.proxy_type == ServerType.mobile
                and server.proxy_privacy == ServerPrivacy.shared
            ):
                proxy_block = await client.get_proxy_block(
                    config_content,
                    proxy_port=old_proxy.get("port_https"),
                    find_by_port=True,
                )
            else:
                proxy_block = await client.get_proxy_block(
                    config_content, old_proxy.get("login"), old_proxy.get("password")
                )

            if not proxy_block:
                logger.info(
                    f"Не нашел прокси блок по логину {old_proxy.get('login')} и паролю {old_proxy.get('password')}"
                )
                # Если блок прокси не найден по логину и паролю, находим по порту
                proxy_block = await client.get_proxy_block(
                    config_content,
                    proxy_port=old_proxy.get("port_https"),
                    find_by_port=True,
                )

            if not proxy_block:
                logger.exception(
                    f"Не могу найти блок прокси по порту {old_proxy.get('port_https')} или по логину {old_proxy.get('login')} и паролю {old_proxy.get('password')}"
                )

            if (
                server.proxy_type == ServerType.mobile
                and server.proxy_privacy == ServerPrivacy.shared
            ):
                logger.info(f"Архивирую шаред прокси {new_proxy.port_https}")
                # Меняем данные в мобильных шаред прокси
                # Удаляем разрешенный ip из блока прокси
                allowed_ips = re.findall("auth iponly([\w\W]*?)proxy", proxy_block)
                if allowed_ip and len(allowed_ips) > 0:
                    allowed_ips = allowed_ips[0].strip()
                    if "," not in allowed_ips:
                        # Если в allow остался последний ip адрес
                        new_allowed_ips = allowed_ips.replace(allowed_ip.ip_address, "")
                    else:
                        new_allowed_ips = allowed_ips.replace(
                            f",{allowed_ip.ip_address}", ""
                        ).replace(f"{allowed_ip.ip_address},", "")
                    new_proxy_block = proxy_block.replace(allowed_ips, new_allowed_ips)
                    logger.info(
                        f"Удалил из блока разрешенный ip {allowed_ip.ip_address}"
                    )
                else:
                    # Если юзер не задал разрешенный ip
                    new_proxy_block = proxy_block
                config_content = config_content.replace(proxy_block, new_proxy_block)
            else:
                logger.info(f"Архивирую приватный прокси {new_proxy.port_https}")
                # Меняем данные во всех остальных видах прокси
                if "iponly" in proxy_block:
                    # Заменяем блок auth iponly на auth strong
                    new_proxy_block = await generate_proxy_string(
                        new_proxy.login,
                        new_proxy.password,
                        new_proxy.port_https,
                        modem_ip,
                        server.inner_ip_address,
                    )
                    logger.info(
                        f"Заменил auth iponly на auth strong с логином {new_proxy.login}"
                    )
                else:
                    # Заменяем в блоке старый логин и пароль на новые
                    if (
                        old_proxy.get("login") in proxy_block
                        and old_proxy.get("password") in proxy_block
                    ):
                        new_proxy_block = (
                            proxy_block.replace(
                                f"users {old_proxy.get('login')}",
                                f"users {new_proxy.login}",
                            )
                            .replace(
                                f"allow {old_proxy.get('login')}",
                                f"allow {new_proxy.login}",
                            )
                            .replace(
                                f"deny {old_proxy.get('login')}",
                                f"deny {new_proxy.login}",
                            )
                            .replace(
                                f"CL:{old_proxy.get('password')}",
                                f"CL:{new_proxy.password}",
                            )
                            .replace("#", "")
                        )
                    else:
                        logger.info("Заменяю данные по регуляркам")
                        login = re.findall("users ([^:]+)", proxy_block)[0].strip()
                        password = re.findall("CL:([^ ]+)allow", proxy_block)[0].strip()
                        new_proxy_block = (
                            proxy_block.replace(
                                f"users {login}", f"users {new_proxy.login}"
                            )
                            .replace(f"allow {login}", f"allow {new_proxy.login}")
                            .replace(f"deny {login}", f"deny {new_proxy.login}")
                            .replace(f"CL:{password}", f"CL:{new_proxy.password}")
                        )
                    logger.info(
                        f"Заменил в блоке логин {old_proxy.get('login')} > {new_proxy.login} и пароль {old_proxy.get('password')} > {new_proxy.password}"
                    )

                config_content = config_content.replace(proxy_block, new_proxy_block)

                if old_proxy.get("reload_link"):  # Заменяем название файла реконнекта
                    old_path = f"/var/www/html/{old_proxy.get('reload_link')}.php"
                    new_path = f"/var/www/html/{new_proxy.reload_link}.php"

                    command = f"mv {old_path} {new_path}"
                    if not await client.execute_command(conn, command):
                        reload_file_content = await generate_reload_file_content(
                            server, modem_ip
                        )
                        await client.write_file(
                            conn,
                            f"/var/www/html/{new_proxy.reload_link}.php",
                            reload_file_content,
                        )
                        logger.info(
                            f"Создан новый файл реконнекта: {new_proxy.reload_link}.php"
                        )
                    else:
                        logger.info(
                            f"Файл реконнекта переименован c {old_proxy.get('reload_link')} на {new_proxy.reload_link}"
                        )

        config_writed = await client.write_file(
            conn, server.config_path, config_content
        )
        logger.info("Внес все изменения в конфиг")

        if (
            server.proxy_type == ServerType.mobile
            and server.proxy_privacy == ServerPrivacy.private
        ):
            await client.write_file(conn, "/usr/local/cron.txt", cron_file_content)
            cron_writed = await client.write_cron(conn)
            logger.info("Внес все изменения в крон")
            return all([config_writed, cron_writed])
        else:
            return config_writed


async def generate_vpn_config(server: VPNServer, telegram_id: int) -> str | bool:
    """Generating Wireguard config for user"""

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False
        logger.info(f"Генерирую конфиг для юзера {telegram_id}")
        if await client.execute_command(
            conn, f"bash wireguard-install.sh {telegram_id}"
        ):
            logger.info("Конфиг сгенерирован")
            vpn_config = await client.read_file(conn, f"{telegram_id}-wg0.conf")
            return vpn_config

        return False


async def delete_vpn_config(server: VPNServer, telegram_id: int) -> bool:
    """Deleting expired Wireguard config"""

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False
        logger.info(f"Удаляю впн конфиг юзера {telegram_id}")
        if await client.execute_command(conn, f"rm {telegram_id}-wg0.conf"):
            return True

        return False
